/*
 * Interpreter.java - ScriptME
 * 
 * Copyright (c) 2009 Cesar Henriques <cesar at alttab.com.ar>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Based on FESI Project
 * 
 * Contributors:
 * 	Jean-Marc Lugrin - initial API and implementation
 * 	Cesar Henriques <cesar at alttab.com.ar> - J2ME Porting and Extensions
 */
package org.scriptme.ecmascript.interpreter;

import javax.microedition.midlet.MIDlet;
import org.scriptme.ecmascript.ext.BasicIOInterface;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Enumeration;
import java.util.Vector;
import org.scriptme.ecmascript.data.ESLoader;
import org.scriptme.ecmascript.data.ESObject;
import org.scriptme.ecmascript.data.ESString;
import org.scriptme.ecmascript.data.ESUndefined;
import org.scriptme.ecmascript.data.ESValue;
import org.scriptme.ecmascript.data.ESWrapper;
import org.scriptme.ecmascript.data.ValueDescription;
import org.scriptme.ecmascript.exceptions.EcmaScriptException;
import org.scriptme.ecmascript.gui.GuiFactory;
import org.scriptme.ecmascript.gui.GuiFactoryImpl;
import org.scriptme.ecmascript.gui.InterpreterCommands;

/**
 * A general purpose interpreter for basic command line and GUI interfaces. Load
 * common extensions and provide various useful if limited commands to evaluate
 * and test scripts.
 * <P>
 * This class can be subclassed, however the interface for subclassing can
 * evolve over time. It may be preferable to write interpreters from scratchs
 * using this one as an example.
 */
public class Interpreter implements InterpreterCommands {

	/** The Constant originalOut. */
	protected static final PrintStream originalOut = System.out;

	/** The Constant originalErr. */
	protected static final PrintStream originalErr = System.err;

	/** The input stream. */
	protected InputStream inputStream = null;

	/** The print stream. */
	protected PrintStream printStream = System.out;

	/** The error stream. */
	protected PrintStream errorStream = System.err;

	/** The version printed. */
	private boolean versionPrinted = false;

	/** The interactive. */
	protected boolean interactive = false;

	/** The any error. */
	protected boolean anyError = false; // Any error in a test file

	/** The any main test. */
	protected boolean anyMainTest = false; // any file processed by -T

	/** The eol. */
	private static String eol = System.getProperty("line.separator") == null ? "\n"
			: System.getProperty("line.separator");

	/** The document. */
	protected ESObject document = null;

	/** The evaluator. */
	protected Evaluator evaluator = new Evaluator();

	/** The gui factory. */
	protected GuiFactory guiFactory = GuiFactoryImpl.getInstance();
	// For interactive evaluation - protected for subclass use ONLY
	/** The last result. */
	protected ESValue lastResult = null; // Means last command did not
											// evaluate anything

	/** The time of eval. */
	protected long timeOfEval = 0; // Valid only if lastResult is not null

	/** The start time. */
	protected long startTime = 0;

	/** The value. */
	protected ESValue theValue = ESUndefined.theUndefined;

	/** The regular expression extension name. */
	protected String regularExpressionExtensionName = "org.scriptme.ecmascript.ext.OptionalRegExp";

	/** The _midlet. */
	protected MIDlet _midlet;

	/**
	 * Create and initialize the interpreter.
	 */
	public Interpreter() {
		super();
	}

	/**
	 * Create and initialize the interpreter.
	 * 
	 * @param midlet
	 *            the midlet
	 */
	public Interpreter(MIDlet midlet) {
		super();
		_midlet = midlet;
	}

	/**
	 * Overridable error exit (can be changed by subclass) IMPLEMENTATION SHOULD
	 * NOT RETURN.
	 */
	protected void errorExit() {
		System.exit(1);
	}

	/**
	 * Exit with a status of 0 if no error was detected and 1 otherwise.
	 */
	public void exit() {
		System.exit(anyError ? 1 : 0);
	}

	/**
	 * Forget any last result of a previous evaluation.
	 */
	void clearLastResult() {
		lastResult = null;
	}

	/**
	 * Do any standard extension initialization. Can be overriden for specific
	 * implementation.
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	protected void loadCommonExtensions() throws EcmaScriptException {
		loadExtension("org.scriptme.ecmascript.ext.JavaAccess");
	}

	/**
	 * Load extension.
	 * 
	 * @param name
	 *            the name
	 * 
	 * @return the object
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	public Object loadExtension(String name) throws EcmaScriptException {
		return evaluator.addMandatoryExtension(name);
	}

	/**
	 * Reset the system (including at initialization).
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	public void reset() throws EcmaScriptException {
		// Reset the main evaluator, forgetting all globals
		evaluator.reset();

		BasicIOInterface basicIO = null;
		try {
			// Initialize GUI main Component
			if (this._midlet != null) {
				guiFactory.setMidlet(this._midlet);
			}

			basicIO = (BasicIOInterface) evaluator
					.addMandatoryExtension("org.scriptme.ecmascript.ext.BasicIO");

			document = basicIO.getDocument();

			loadCommonExtensions();
		} catch (EcmaScriptException e) {
			errorStream.println("Cannot initialize BasicIO - exiting: " + eol
					+ e);
			e.printStackTrace();
			errorExit();
		}

	}

	/**
	 * Print standard version text.
	 */
	protected void printVersion() {
		printStream.println(Evaluator.getWelcomeText());
		versionPrinted = true;
	}

	/**
	 * Display standard about text (for GUI).
	 */
	public void displayAboutText() {
		printAbout();
	}

	/**
	 * Define the title of the window (to help implementing subclasses).
	 * 
	 * @return the title
	 */
	protected String getTitle() {
		return "ScriptME - A minimal EcmaScript interpreter";
	}

	/**
	 * Print standard ABOUT text.
	 */
	protected void printAbout() {
		printVersion();
		printStream.println();
		printStream
				.println("Provided 'as is', use at your own risk, no support available.");
		printStream
				.println("Can be freely used and redistributed as long as this notice is included.");
		printStream
				.println("Feedback may be sent to 'cesar@technocracy.com.ar', but I may not");
		printStream.println("be able (or willing) to answer all mails.");
	}

	/**
	 * Do the command load, loading from a module (via a path).
	 * 
	 * @param moduleName
	 *            module to load
	 */
	protected void doLoad(String moduleName) {
		if (moduleName == null || moduleName.equals("")) {
			printStream.println("[[Module name missing for @load.]]");
			return;
		}
		try {
			try {
				document.putHiddenProperty("URL", new ESString("module://"
						+ moduleName));
			} catch (EcmaScriptException ignore) {
			}
			printStream.println("@@ Loading module '" + moduleName + "' . . .");
			startTime = System.currentTimeMillis();

			theValue = evaluator.evaluateLoadModule(moduleName); // EVALUATION

			timeOfEval = System.currentTimeMillis() - startTime;
			lastResult = theValue;
			if (theValue != null) {
				printStream.println("@@ Resulting in: " + theValue);
			}
		} catch (EcmaScriptException e) {
			printStream.println("[[Error loading the module '" + moduleName
					+ "']]");
			printStream.println("[[Error: " + e.getMessage() + "]]");
		} catch (Exception e) {
			printStream.println("[[**Uncatched error: " + e + "]]");
			e.printStackTrace();
		} finally {
			try {
				document.putHiddenProperty("URL", new ESString(
						"module://<stdin>"));
			} catch (EcmaScriptException ignore) {
			}
		}
	}

	/**
	 * Execute a string - return the line number of any error or 0 Called by the
	 * GUI to load a buffer content.
	 * 
	 * @param text
	 *            The text to execute
	 * @param source
	 *            The identification of the source
	 * 
	 * @return the line number of any error if possible or 0
	 */
	public int executeString(String text, String source) {
		try {
			try {
				document.putHiddenProperty("URL", new ESString("source://"
						+ source));
			} catch (EcmaScriptException ignore) {
			}
			printStream.println("@@ Executing '" + source + "' . . .");
			theValue = evaluator.evaluate(text, source); // EVALUATION
			if (interactive && theValue != null) {
				printStream.println("@@ Resulting in: " + theValue);
			}

		} catch (EcmaScriptException e) {
			errorStream.println(e.getMessage() + eol + "[[Error executing '"
					+ source + "']]");
			return e.getLineNumber();
		} finally {
			try {
				document.putHiddenProperty("URL",
						new ESString("file://<stdin>"));
			} catch (EcmaScriptException ignore) {
			}
		}
		return 0;
	}

	/**
	 * Expand an html source file containing <script> commands.
	 * 
	 * @param htmlContent
	 *            the html content
	 */
	protected void doExpand(String htmlContent) {
	}

	/**
	 * Execute a fragment to test in a controlled environment.
	 * 
	 * @param currentTest
	 *            Name of the test to execute
	 * @param scriptString
	 *            The fragment to test
	 * 
	 * @return true, if test string
	 */
	protected boolean testString(String currentTest, String scriptString) {
		printStream.println("@@ Testing " + currentTest);
		try {
			ESValue theValue = evaluator.evaluate(scriptString);
			if (theValue == null) {
				throw new EcmaScriptException("No value returned from @test");
			}
			boolean success = false;
			try {
				success = theValue.booleanValue();
			} catch (EcmaScriptException e) {
				throw new EcmaScriptException(
						"@test did not return a boolean value");
			}
			if (!success) {
				throw new EcmaScriptException("@test did not return 'true'");
			}
			return true;
		} catch (Exception e) {
			printStream.println("[[Test " + currentTest + " failed]]");
			printStream.println("[[Error: " + e.getMessage() + "]]");
			return false;
		}
	}

	/**
	 * Print details of last execution.
	 */
	protected void printDetail() {
		if (lastResult == null) {
			printStream.println("** No last result available");
		} else {
			printStream.println("** Result: " + lastResult.toDetailString());
			printStream.println("** Evaluated in " + timeOfEval
					+ " ms (note: +/-20 ms precision!)");
		}
	}

	/**
	 * Toggle debug parse.
	 */
	public void toggleDebugParse() {
		evaluator.setDebugParse(!evaluator.isDebugParse());
		printStream
				.println("@@ debugParse is now: " + evaluator.isDebugParse());
	}

	/**
	 * Toggle debug java access.
	 */
	public void toggleDebugJavaAccess() {
		ESLoader.setDebugJavaAccess(!ESLoader.isDebugJavaAccess());
		printStream.println("@@ debugJavaAccess is now: "
				+ ESLoader.isDebugJavaAccess());
	}

	/**
	 * Toggle debug loader.
	 */
	public void toggleDebugLoader() {
		ESLoader.setDebugLoader(!ESLoader.isDebugLoader());
		printStream.println("@@ debugLoader is now: "
				+ ESLoader.isDebugLoader());
	}

	/**
	 * Toggle debug event.
	 */
	public void toggleDebugEvent() {
		ESWrapper.setDebugEvent(!ESWrapper.isDebugEvent());
		printStream.println("@@ debugJavaAccess is now: "
				+ ESWrapper.isDebugEvent());
	}

	/**
	 * List the loaded extensions.
	 */
	protected void listExtensions() {
		int i = 0;
		for (Enumeration e = evaluator.getExtensions(); e.hasMoreElements();) {
			printStream.println(" " + e.nextElement());
			i++;
		}
		printStream.println(" " + i + " extensions loaded");
	}

	/**
	 * Display the
	 * 
	 * @help text information (called by the GUI).
	 */
	public void displayHelpText() {
		printHelp();
	}

	/**
	 * Print the help message on the print stream.
	 */
	protected void printHelp() {
		Command.printHelp(printStream);
	}

	/**
	 * Reset the evaluator, forgetting all global variables.
	 */
	protected void resetEvaluator() {
		try {
			printStream.println("@@ Reseting global object to default values");
			reset();
		} catch (EcmaScriptException e) {
			errorStream.println("[[Error during initialization: "
					+ e.getMessage() + "]]");
			e.printStackTrace();
			return;
		}
	}

	/**
	 * List the free memory.
	 */
	protected void listMemory() {
		Runtime rt = Runtime.getRuntime();
		long fm = rt.freeMemory();
		long tm = rt.totalMemory();
		printStream.println("@@ Total memory: " + tm + ", free memory: " + fm);
	}

	/**
	 * List the visible properties of an object.
	 * 
	 * @param parameter
	 *            The expression returning an object on which to list the
	 *            properties
	 */
	protected void listProperties(String parameter) {
		ESObject listObject = evaluator.getGlobalObject();
		try {
			if (parameter != null) {
				ESValue listValue = evaluator.evaluate(parameter);
				if (!(listValue instanceof ESObject)) {
					printStream.println("Cannot evaluate '" + parameter
							+ "' to an object");
					return;
				}
				listObject = (ESObject) listValue;
			}
			boolean directEnumeration = listObject.isDirectEnumerator();
			for (Enumeration e = listObject.getProperties(); e
					.hasMoreElements();) {
				String property = e.nextElement().toString();
				printStream.print(property);
				if (!directEnumeration) {
					String propertyValue = listObject.getProperty(property,
							property.hashCode()).toString();
					// Remove leading eol
					while (propertyValue.indexOf("\n") == 0) {
						propertyValue = propertyValue.substring(1);
					}
					while (propertyValue.indexOf(eol) == 0) {
						propertyValue = propertyValue.substring(eol.length());
					}
					// limit size
					if (propertyValue.length() > 250) {
						propertyValue = propertyValue.substring(0, 250) + "...";
					}
					// keep only first line
					int ieol = propertyValue.indexOf(eol);
					if (ieol == -1) {
						ieol = propertyValue.indexOf("\n");
					}
					if (ieol != -1) {
						propertyValue = propertyValue.substring(0, ieol)
								+ "...";
					}
					printStream.println(": " + propertyValue);
				}
			}
		} catch (Exception e) {
			printStream.println("Cannot evaluate '" + parameter
					+ "' properties");
			printStream.println(e);
		}
	}

	/**
	 * List all propoerties of an object (visible or not).
	 * 
	 * @param parameter
	 *            The expression returning an object on which to list the
	 *            properties
	 */
	protected void listAllProperties(String parameter) {
		ESObject listObject = evaluator.getGlobalObject();
		try {
			if (parameter != null) {
				ESValue listValue = evaluator.evaluate(parameter);
				if (!(listValue instanceof ESObject)) {
					printStream.println("Cannot evaluate '" + parameter
							+ "' to an object");
					return;
				}
				listObject = (ESObject) listValue;
			}
			for (Enumeration e = listObject.getAllProperties(); e
					.hasMoreElements();) {
				String property = e.nextElement().toString();
				ESValue propertyValue = listObject.getProperty(property,
						property.hashCode());
				printStream.println(propertyValue.getDescription(property));
			}
		} catch (Exception e) {
			printStream.println("Cannot evaluate '" + parameter
					+ "' properties");
			printStream.println(e);
		}
	}

	/**
	 * Describe an object.
	 * 
	 * @param parameter
	 *            the parameter
	 */
	protected void describe(String parameter) {
		ESValue toBeDescribed = evaluator.getGlobalObject();
		try {
			if (parameter != null) {
				toBeDescribed = evaluator.evaluate(parameter);
			} else {
				parameter = "global";
			}
			printStream.println(toBeDescribed.getDescription(parameter));

			if (toBeDescribed.isComposite()) {
				for (Enumeration e = toBeDescribed.getAllDescriptions(); e
						.hasMoreElements();) {
					ValueDescription description = (ValueDescription) e
							.nextElement();
					printStream.println("   " + description.toString());
				}
			}
		} catch (Exception e) {
			printStream.println("Cannot evaluate '" + parameter + "'");
			printStream.println(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.gui.InterpreterCommands#loadFile(java.lang.String,
	 *      java.lang.String)
	 */
	public void loadFile(String directoryName, String fileName) {
	}

	/**
	 * Evaluate.
	 * 
	 * @param scriptString
	 *            the script string
	 * @param acceptReturn
	 *            the accept return
	 * 
	 * @return the eS value
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	public ESValue evaluate(String scriptString, boolean acceptReturn)
			throws EcmaScriptException {
		ESValue retVal = evaluator.evaluate(scriptString, null, acceptReturn);
		if (retVal == null) {
			throw new EcmaScriptException(
					"No value returned from script evaluation");
		}
		return retVal;
	}

	/**
	 * Evaluate.
	 * 
	 * @param scriptString
	 *            the script string
	 * @param source
	 *            the source
	 * 
	 * @return the eS value
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	public ESValue evaluate(String scriptString, String source)
			throws EcmaScriptException {
		ESValue retVal = evaluator.evaluate(scriptString, source);
		if (retVal == null) {
			throw new EcmaScriptException(
					"No value returned from script evaluation");
		}
		return retVal;
	}

	/**
	 * Evaluate.
	 * 
	 * @param scriptString
	 *            the script string
	 * @param source
	 *            the source
	 * @param acceptReturn
	 *            the accept return
	 * 
	 * @return the eS value
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 */
	public ESValue evaluate(String scriptString, String source,
			boolean acceptReturn) throws EcmaScriptException {
		ESValue retVal = evaluator.evaluateSource(scriptString, source,
				acceptReturn);
		if (retVal == null) {
			throw new EcmaScriptException(
					"No value returned from script evaluation");
		}
		return retVal;
	}
}

/**
 * Common code for all commands
 */
abstract class Command {

	/**
	 * Keep track of all commands
	 */
	static private Vector allCommands = new Vector();
	protected String name;
	protected String lowerCaseName; // For case insensitive search
	protected String help;

	/**
	 * Print the help text of a command
	 */
	static void printHelp(PrintStream printStream) {
		for (Enumeration e = allCommands.elements(); e.hasMoreElements();) {
			Command cmd = (Command) e.nextElement();
			printStream.println("  @" + cmd.name + " - " + cmd.help);
		}
	}

	/**
	 * Execute a command
	 * 
	 * @param interpreter
	 *            The interpreter context
	 * @param printStream
	 *            The output stream to use
	 * @param command
	 *            the command name (as typed)
	 * @param parameter
	 *            the command parameter string
	 */
	static boolean executeCommand(Interpreter interpreter,
			PrintStream printStream, String command, String parameter) {
		Vector foundCmds = new Vector();
		String lcCommand = command.toLowerCase();
		for (Enumeration e = allCommands.elements(); e.hasMoreElements();) {
			Command cmd = (Command) e.nextElement();
			if (cmd.lowerCaseName.equals(command)) {
				// Exact same name, execute
				return cmd.doCommand(interpreter, parameter);
			} else if (cmd.lowerCaseName.startsWith(command)) {
				// Prefix, add to list of candidates
				foundCmds.addElement(cmd);
			}
		}
		if (foundCmds.size() == 0) {
			printStream.println("@@ Command '" + command + "' not recognized");
			interpreter.printHelp();
		} else if (foundCmds.size() == 1) {
			Command cmd = (Command) foundCmds.elementAt(0);
			return cmd.doCommand(interpreter, parameter);
		} else {
			printStream
					.println("@@ Command More than one command starting with '"
							+ command + "'");
			for (Enumeration e = foundCmds.elements(); e.hasMoreElements();) {
				Command cmd = (Command) e.nextElement();
				printStream.println("  @" + cmd.name + " - " + cmd.help);
			}
		}
		return false;
	}

	/**
	 * Define a new command by name with a specified help string
	 */
	Command(String name, String help) {
		this.name = name;
		this.lowerCaseName = "@" + name.toLowerCase();
		this.help = help;
		allCommands.addElement(this);
	}

	/**
	 * Execute a command with the specified parameter
	 * 
	 * @param interpreter
	 *            the interpreter context
	 * @param parameter
	 *            The string parameter
	 * @return true if the interpreter must exit
	 */
	abstract boolean doCommand(Interpreter interpreter, String parameter);
}
